Repository navigation
feat(group): member-add-mode, join-approval-mode, participant requests + message forward - #2595
Conversation
…s + message forward
Exposes Baileys group/message capabilities that already exist in the underlying
library but had no REST endpoints:
- POST /group/memberAddMode -> groupMemberAddMode (admin_add | all_member_add)
- POST /group/joinApprovalMode -> groupJoinApprovalMode (on | off)
- GET /group/participantRequests -> groupRequestParticipantsList (pending join requests)
- POST /group/updateParticipantRequests -> groupRequestParticipantsUpdate (approve | reject)
- POST /message/forwardMessage -> sendMessage(jid, { forward }) (keeps isForwarded)
Each follows the existing pattern (DTO + JSONSchema + router + controller +
baileys service). No new dependencies — baileys 7.0.0-rc.9 already ships these.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reviewer's GuideAdds REST endpoints and DTO/schema/controller/service plumbing to expose new WhatsApp group admin capabilities (member-add restrictions, join approval, participant request management) and a message-forwarding endpoint that reuses stored WAMessages and Baileys forwarding, following existing validation/router patterns. Sequence diagram for forwarding a stored WhatsApp messagesequenceDiagram
actor Client
participant MessageRouter
participant SendMessageController
participant BaileysStartupService
participant BaileysClient
Client->>MessageRouter: POST /message/forwardMessage
MessageRouter->>MessageRouter: dataValidate ForwardMessageDto
MessageRouter->>SendMessageController: forwardMessage(instance, data)
SendMessageController->>BaileysStartupService: forwardMessage(data)
BaileysStartupService->>BaileysStartupService: getMessage({ id: messageId }, true)
BaileysStartupService->>BaileysClient: sendMessage(jid, { forward: fullMsg })
BaileysClient-->>BaileysStartupService: sendMessage result
BaileysStartupService-->>SendMessageController: forward result
SendMessageController-->>MessageRouter: response
MessageRouter-->>Client: 201 Created
Sequence diagram for group participant request management endpointssequenceDiagram
actor Admin
participant GroupRouter
participant GroupController
participant BaileysStartupService
participant BaileysClient
rect rgb(235, 245, 255)
Admin->>GroupRouter: GET /group/participantRequests
GroupRouter->>GroupRouter: groupValidate GroupJid
GroupRouter->>GroupController: findParticipantRequests(instance, groupJid)
GroupController->>BaileysStartupService: findParticipantRequests(groupJid)
BaileysStartupService->>BaileysClient: groupRequestParticipantsList(groupJid)
BaileysClient-->>BaileysStartupService: requests
BaileysStartupService-->>GroupController: { requests }
GroupController-->>GroupRouter: { requests }
GroupRouter-->>Admin: 200 OK
end
rect rgb(235, 255, 235)
Admin->>GroupRouter: POST /group/updateParticipantRequests
GroupRouter->>GroupRouter: groupValidate GroupUpdateParticipantRequestDto
GroupRouter->>GroupController: updateParticipantRequests(instance, update)
GroupController->>BaileysStartupService: updateParticipantRequests(update)
BaileysStartupService->>BaileysClient: groupRequestParticipantsUpdate(groupJid, participants, action)
BaileysClient-->>BaileysStartupService: update result
BaileysStartupService-->>GroupController: { updateParticipantRequests: result }
GroupController-->>GroupRouter: response
GroupRouter-->>Admin: 201 Created
end
File-Level Changes
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 1 issue, and left some high level feedback:
- In
forwardMessage, the innerthrow new BadRequestException('Message not found')is immediately caught and wrapped as a generic'Error forwarding message', so callers lose the specific error; consider either not catching that case or rethrowing it unchanged whenfullMsg?.messageis missing. - The logic to normalize phone numbers/JIDs (e.g.,
number.replace(/\D/g, '')and appending@s.whatsapp.netinforwardMessageandupdateParticipantRequests) appears ad hoc; consider extracting and reusing a shared helper to keep JID handling consistent across endpoints. - In
updateParticipantRequestSchema,participantsis required but not included in theisNotEmptycall, so empty strings would pass validation; it may be worth addingparticipantstoisNotEmptyor tightening the item schema if you want to reject blank participant entries.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In `forwardMessage`, the inner `throw new BadRequestException('Message not found')` is immediately caught and wrapped as a generic `'Error forwarding message'`, so callers lose the specific error; consider either not catching that case or rethrowing it unchanged when `fullMsg?.message` is missing.
- The logic to normalize phone numbers/JIDs (e.g., `number.replace(/\D/g, '')` and appending `@s.whatsapp.net` in `forwardMessage` and `updateParticipantRequests`) appears ad hoc; consider extracting and reusing a shared helper to keep JID handling consistent across endpoints.
- In `updateParticipantRequestSchema`, `participants` is required but not included in the `isNotEmpty` call, so empty strings would pass validation; it may be worth adding `participants` to `isNotEmpty` or tightening the item schema if you want to reject blank participant entries.
## Individual Comments
### Comment 1
<location path="src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts" line_range="4647" />
<code_context>
+
+ public async updateParticipantRequests(update: GroupUpdateParticipantRequestDto) {
+ try {
+ const participants = update.participants.map((p) => (p.includes('@') ? p : `${p}@s.whatsapp.net`));
+ const result = await this.client.groupRequestParticipantsUpdate(update.groupJid, participants, update.action);
+ return { updateParticipantRequests: result };
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Participant JID normalization is inconsistent with number normalization elsewhere and may accept malformed identifiers.
Participants are treated as JIDs whenever they contain `@`, and otherwise `@s.whatsapp.net` is appended, but there’s no stripping/validation of the local part. If callers pass phone numbers with spaces, dashes, or other formatting, this will produce invalid JIDs. Please either reuse the same normalization helper used in `forwardMessage` (or similar) or enforce that inputs are already-normalized JIDs or strictly numeric IDs before appending the domain.
Suggested implementation:
```typescript
public async updateParticipantRequests(update: GroupUpdateParticipantRequestDto) {
try {
const normalizeParticipant = (raw: string): string => {
const value = raw.trim();
if (!value) {
throw new BadRequestException('Participant identifier cannot be empty');
}
// If it's already a JID, return as-is (after trimming)
if (value.includes('@')) {
return value;
}
// Treat as phone-like input: strip non-digits and validate
const digitsOnly = value.replace(/\D/g, '');
if (!digitsOnly) {
throw new BadRequestException(
`Invalid participant identifier "${raw}". Expected a JID or numeric phone number.`,
);
}
return `${digitsOnly}@s.whatsapp.net`;
};
const participants = update.participants.map(normalizeParticipant);
const result = await this.client.groupRequestParticipantsUpdate(update.groupJid, participants, update.action);
return { updateParticipantRequests: result };
} catch (error) {
throw new BadRequestException('Error updating participant requests', error.toString());
}
}
```
If your codebase already has a shared normalization helper (e.g. used in `forwardMessage`), consider:
1. Replacing the in-method `normalizeParticipant` function with a call to that shared helper for consistency.
2. If that helper returns bare numbers instead of full JIDs, adjust the logic to append `@s.whatsapp.net` only when necessary, keeping the validation/stripping behavior consistent.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
|
|
||
| public async updateParticipantRequests(update: GroupUpdateParticipantRequestDto) { | ||
| try { | ||
| const participants = update.participants.map((p) => (p.includes('@') ? p : `${p}@s.whatsapp.net`)); |
There was a problem hiding this comment.
suggestion (bug_risk): Participant JID normalization is inconsistent with number normalization elsewhere and may accept malformed identifiers.
Participants are treated as JIDs whenever they contain @, and otherwise @s.whatsapp.net is appended, but there’s no stripping/validation of the local part. If callers pass phone numbers with spaces, dashes, or other formatting, this will produce invalid JIDs. Please either reuse the same normalization helper used in forwardMessage (or similar) or enforce that inputs are already-normalized JIDs or strictly numeric IDs before appending the domain.
Suggested implementation:
public async updateParticipantRequests(update: GroupUpdateParticipantRequestDto) {
try {
const normalizeParticipant = (raw: string): string => {
const value = raw.trim();
if (!value) {
throw new BadRequestException('Participant identifier cannot be empty');
}
// If it's already a JID, return as-is (after trimming)
if (value.includes('@')) {
return value;
}
// Treat as phone-like input: strip non-digits and validate
const digitsOnly = value.replace(/\D/g, '');
if (!digitsOnly) {
throw new BadRequestException(
`Invalid participant identifier "${raw}". Expected a JID or numeric phone number.`,
);
}
return `${digitsOnly}@s.whatsapp.net`;
};
const participants = update.participants.map(normalizeParticipant);
const result = await this.client.groupRequestParticipantsUpdate(update.groupJid, participants, update.action);
return { updateParticipantRequests: result };
} catch (error) {
throw new BadRequestException('Error updating participant requests', error.toString());
}
}If your codebase already has a shared normalization helper (e.g. used in forwardMessage), consider:
- Replacing the in-method
normalizeParticipantfunction with a call to that shared helper for consistency. - If that helper returns bare numbers instead of full JIDs, adjust the logic to append
@s.whatsapp.netonly when necessary, keeping the validation/stripping behavior consistent.
|
🔴 👎 A feature de grupo (member-add-mode, join-approval, participant requests) parece útil e aditiva, mas o PR aponta para |
… de grupos) + baileys rc13
… profilePicture — listagem sem rajada)
| "node_modules/libsignal": { | ||
| "name": "@whiskeysockets/libsignal-node", | ||
| "version": "2.0.1", | ||
| "resolved": "git+ssh://git@github.com/whiskeysockets/libsignal-node.git#1c30d7d7e76a3b0aa120b04dc6a26f5a12dccf67", | ||
| "version": "6.0.0", | ||
| "resolved": "https://registry.npmjs.org/libsignal/-/libsignal-6.0.0.tgz", | ||
| "integrity": "sha512-d/5V3YFtDljbFMufz4ncyUYGYhJl+vzAe+c2EFFBQ6bz1h8Q3IOMEGXYMzlibU60I+e8GagMMpji18iez3P1hA==", | ||
| "license": "GPL-3.0", | ||
| "dependencies": { | ||
| "curve25519-js": "^0.0.4", | ||
| "protobufjs": "6.8.8" | ||
| } | ||
| }, | ||
| "node_modules/libsignal/node_modules/@types/node": { | ||
| "version": "10.17.60", | ||
| "resolved": "https://registry.npmjs.org/@types/node/-/node-10.17.60.tgz", | ||
| "integrity": "sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==", | ||
| "license": "MIT" | ||
| }, | ||
| "node_modules/libsignal/node_modules/long": { | ||
| "version": "4.0.0", | ||
| "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", | ||
| "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", | ||
| "license": "Apache-2.0" | ||
| }, | ||
| "node_modules/libsignal/node_modules/protobufjs": { | ||
| "version": "6.8.8", | ||
| "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.8.8.tgz", | ||
| "integrity": "sha512-AAmHtD5pXgZfi7GMpllpO3q1Xw1OYldr+dMUlAnffGTAhqkg72WdmSY71uKBF/JuyiKs8psYbtKrhi0ASCD8qw==", | ||
| "hasInstallScript": true, | ||
| "license": "BSD-3-Clause", | ||
| "dependencies": { | ||
| "@protobufjs/aspromise": "^1.1.2", | ||
| "@protobufjs/base64": "^1.1.2", | ||
| "@protobufjs/codegen": "^2.0.4", | ||
| "@protobufjs/eventemitter": "^1.1.0", | ||
| "@protobufjs/fetch": "^1.1.0", | ||
| "@protobufjs/float": "^1.0.2", | ||
| "@protobufjs/inquire": "^1.1.0", | ||
| "@protobufjs/path": "^1.1.2", | ||
| "@protobufjs/pool": "^1.1.0", | ||
| "@protobufjs/utf8": "^1.1.0", | ||
| "@types/long": "^4.0.0", | ||
| "@types/node": "^10.1.0", | ||
| "long": "^4.0.0" | ||
| }, | ||
| "bin": { | ||
| "pbjs": "bin/pbjs", | ||
| "pbts": "bin/pbts" | ||
| "protobufjs": "^7.5.5" | ||
| } | ||
| }, |
There was a problem hiding this comment.
security (license/libsignal): GPL-3.0-only: Open-source license can require releasing the entire application source
This GPL-3.0-only open-source license can impose strong copyleft or non-commercial obligations that may require releasing your full application source code or restrict commercial use, depending on how the code is used or distributed
Source: trivy
|
need to appoint to develop (not to main) @alpesdigital-adm |
Co-authored-by: root <root@srv1512423.hstgr.cloud>
chats.update arrives keyed by LID on accounts migrated to LID addressing, while inbound messages are already normalized to the phone JID. Webhook consumers could not match the read state to the conversation, so reading on the phone never cleared unread counts downstream. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Webhook consumers only see the phone JID, but LID-addressed chats are indexed by LID on the primary device. Receipts and app-state patches sent to the phone JID target a chat the phone does not know. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The auth-state adapters rebuilt AppStateSyncKeyData with create(), which keeps keyData as the stored base64 string. Baileys then derived the app-state keys from the 44-char text instead of the 32 key bytes: every incoming patch failed to decrypt and was skipped silently (MAC checks are off by default), and every outgoing chatModify patch was encrypted with the wrong keys, which the server answers with 401 device_removed. fromObject decodes base64 into bytes, as Baileys useMultiFileAuthState. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…(CVE pending, evolution-foundation#2435) (evolution-foundation#2549) * security: prevent cross-instance auth bypass via query/body override (evolution-foundation#2435) The auth guard in src/api/guards/auth.guard.ts validates instance ownership using req.params.instanceName. The abstract router then merged req.query (and, on /instance/create, req.body) into the instance object via Object.assign — which silently overwrote the already-authenticated instanceName. An attacker with one valid token could send: GET /chat/findMessages/MY_INSTANCE?instanceName=VICTIM_INSTANCE Auth passed for MY_INSTANCE, but dataValidate() then replaced the instance with VICTIM_INSTANCE before execute() ran — giving the caller full access to read/send messages, modify settings, and delete other tenants' instances. CWE-639: Authorization Bypass Through User-Controlled Key Fix: introduce sanitizeUntrustedInput() that strips PROTECTED_INSTANCE_FIELDS (instanceName, instanceId) from any untrusted source before merging into the instance object. Logs a warning on attempts so abuse is auditable. Closes evolution-foundation#2435 Reported by @lighthousekeeper1212 via static analysis. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * chore(lint): remove extra blank line introduced by recent merge Pre-push lint flagged whatsapp.baileys.service.ts:531 (double blank line after the stream:error 515 fix merged via evolution-foundation#2509). Trivial prettier fix. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> (cherry picked from commit 7a55a2b)
…m:error 515 (evolution-foundation#2509) * fix(baileys): prevent healthy instances from being killed after stream:error 515 When WhatsApp sends stream:error code=515 (Connection Replaced), Baileys handles the reconnect correctly and fires connection.update with state='open'. However, WhatsApp then sends a 401 (loggedOut) to clean up the old session slot, which Evolution API incorrectly treated as a real logout, killing the newly-connected healthy instance. The fix tracks when a stream:error 515 node arrives via the CB:stream:error WebSocket event. If a loggedOut (401) close event fires within 30 seconds of a 515, it is treated as a transient reconnect rather than a real logout. Fixes evolution-foundation#2498 * fix(baileys): name the 515 reconnect grace + tighten stream-error types Addresses sourcery-ai review feedback on the previous commit: - Extract the 30 000ms reconnect grace window into a named class constant STREAM_515_RECONNECT_GRACE_MS so future tuning is self-documenting rather than a literal scattered through the close handler. - Extract the magic '515' string into STREAM_ERROR_CODE_RECONNECT. - Replace the loose 'node: any' on the 'CB:stream:error' handler with a minimal structural type ({ attrs?: { code?: string | number } }) so the payload shape is documented and type-checked. - Compare the code via String(...) so a numeric 515 from the underlying socket library still triggers the grace window — the original literal '515' check would have silently broken on a type change. * fix(baileys): satisfy prettier — collapse 515 reconnect guard onto one line Check Code Quality lint failed on prettier/prettier (the recentStream515 expression fits within the project's 120-col printWidth on a single line, while shouldReconnect still needs to break across two). Co-authored-by: Octopus <liyuan851277048@icloud.com> --------- Co-authored-by: octo-patch <octo-patch@github.com> Co-authored-by: octo-patch <octo-patch@users.noreply.github.com> (cherry picked from commit 1d38d4f)
sanitizeUntrustedInput on /instance/create removed instanceName from the body, which is the only source of the new instance name. Create has no authenticated instance in the path, so there is nothing to override there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Nests the tc token under the picture query in profilePictureUrl, so profile pictures of privacy-restricted contacts resolve. The Android browser mode is opt-in by browser name and stays off here. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
History sync after a fresh pairing (and some live messages) deliver LID-only keys. Without the phone JID, webhook consumers cannot identify the sender and drop the message. Resolve it with getPNForLID, live and in messaging-history.set; unknown mappings leave the key untouched. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
History messages without a known contact got pushName = JID user part (LID digits included), which CRMs then stored as the contact name. Look the contact up by the phone JID as well and otherwise leave it empty. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Em grupo o recibo de leitura endereca quem enviou, nao a conversa: o Baileys
agrupa recibo por `remoteJid:participant` e recusa um markRead cujo key de
mensagem recebida nao tenha participant ("Expected not from me message to have
participant", Utils/chat-utils). A key chegava aqui e era remontada sem esse
campo, entao nenhuma marcacao de leitura de grupo podia funcionar.
Agora o campo e declarado no DTO e no schema de entrada, repassado como veio do
provider, e a ausencia dele vira 400 com o campo nomeado em vez de erro opaco
do SDK — mas so no caminho com lastMessage, que e o que o Baileys realmente
recusa. Pedido legado so com recibo (Chatwoot) segue aceito como antes.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XxnyvfDrgNppE1PftV4gTa
…-self, keep participant on read
1. App-state self-heal. Baileys decodePatches only warns on an LTHash mismatch
("LTHash verification failed, skipping remaining patches"), then returns the
already-advanced version with the mismatching hash and the caller persists it.
Every later patch then fails the same way, so phone read/unread changes
(regular_low) never become chats.update. The logger handed to makeWASocket is
now wrapped (Proxy, works at any log level, covers child loggers) to spot that
exact warning; a per-instance poller (outside Baileys' sync transaction) then
clears app-state-sync-version-<name> through the socket key store (null, in a
transaction on the same mutex key as resyncAppState, so the in-memory cache and
Redis are both cleared) and runs resyncAppState([name], false) so the snapshot
rebuilds the hash and its mutations still flow to chats.update. Guards: socket
must be open, 10 min cooldown and 3 attempts/hour per collection, WARN log
"app-state self-heal: <instance> <collection> v<old> -> snapshot" plus outcome.
The snapshot variant of the warning is deliberately ignored. MAC verification
stays enabled.
2. Group read-self. Baileys emits message-receipt.update (not messages.update) for
a group read; the reader is receipt.userJid (the node's participant), key.fromMe
is always true. When the reader is the instance's own account (phone JID or LID,
device suffix stripped) and groupsIgnore is off, send one MESSAGES_UPDATE READ
webhook per group per batch (fromMe=false, keyId = newest message of the batch)
so the CRM group fallback fires. Receipts from other participants are ignored.
updateMessagesReadedByTimestamp is unchanged.
3. syncMessageReadState keeps `participant` on readMessages keys (needed by
WhatsApp for group receipts); DTO and request schema accept it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…or leave the collection cleared 1. A resync from a cleared version has no minimum version, so Baileys rc14 (processSyncAction, isInitialSync=false, no `conditional`) re-emits every record of the snapshot: stale markChatAsRead -> chats.update unreadCount 0/-1 that the CRM applies as "observed now" (clears chats that still have unread messages / re-marks read ones unread); regular_high replays chats.delete and messages.delete for every chat/message ever deleted. Events chats.update, chats.delete, chats.lock, messages.update and messages.delete emitted from inside the heal resync are now dropped (AsyncLocalStorage scope + guard on socket.ev.emit, so concurrent live events pass); the count is logged. isInitialSync=true was not used: its conditional chat updates stay buffered and swallow later live updates for the same chats. 2. If the heal resync stores nothing (missing key -> parked, giving up) or throws, the previous state is written back (only if nothing was stored meanwhile, same tx key). Leaving it cleared made the next ordinary resync (server_sync / key arrival, isInitialSync=false) replay the whole snapshot unguarded. Tests: 39 node:test pass; tsc clean against baileys 7.0.0-rc13 (local) and 7.0.0-rc14 (production pin) types. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
WhatsApp now delivers some accounts' contact statuses as a <status> stanza
instead of <message from="status@broadcast">. Baileys 7.0.0-rc14/master
registers no CB:status handler, so it is never acked; ~50 min after the
connection opens the server closes the stream with
<stream:error><ack class="status" type="media" id=.../></stream:error>
(Stream Errored (ack) -> badSession 500), redelivers the same pending stanza
on reconnect, and the drop repeats. readStatus/shouldIgnoreJid do not matter:
the stanza never reaches handleMessage.
Register ws.on('CB:status') after makeWASocket and send a plain ack (class =
node tag, id, to, participant/recipient/type, no error attribute, same as
Baileys' buildAckStanza; a NACK would keep it pending). Same fix as upstream
WhiskeySockets/Baileys#2858 / PR #2866 and whatsmeow a238105488; remove once a
Baileys release includes it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…loop - attachStatusAck skips (returns false) when the socket already has a CB:status listener, i.e. a Baileys release that ships #2866 and acks the stanza itself; the service logs that the module can be dropped. - Baileys' sendNode encodes synchronously before returning the send promise, so a sync throw is now caught and reported instead of escaping ws.emit. Verified against a real 7.0.0-rc14 socket: no CB:status listener of its own, the ack goes out as <ack id to class='status' type='media'/>, identical to the PR #2866 test and whatsmeow sendAck. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Baileys onWhatsApp only uses the USync contact protocol, so the endpoint never returned the account LID. With `includeLid: true`, confirmed numbers (exists) are resolved in ONE lidMapping.getLIDsForPNs call (cache -> auth keys -> USync LID query) and each item gets `lid: "<digits>@lid"` or null. - default response unchanged when the flag is absent - lookup is keyed by the jid WhatsApp confirmed (9th-digit variants) - capped at 50 numbers per request; the rest return null with a WARN - a failing lookup logs WARN and returns lid null, never fails the request - runs after the IsOnWhatsapp cache write; that column keeps its 'lid' marker Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
What
Adds REST endpoints for group/message features that baileys (the underlying lib) already implements but Evolution doesn't expose yet:
POST /group/memberAddModegroupMemberAddMode— restrict who adds members (admin_add/all_member_add)POST /group/joinApprovalModegroupJoinApprovalMode— turn membership approval on/off (on/off)GET /group/participantRequestsgroupRequestParticipantsList— list pending join requestsPOST /group/updateParticipantRequestsgroupRequestParticipantsUpdate— approve/reject requestsPOST /message/forwardMessagesendMessage(jid, { forward })— forward a stored message (keeps the forwarded tag)Why
Common group-admin needs (membership approval, locking member-add) and message forwarding. All supported by
baileys(7.0.0-rc.9in this repo) but with no HTTP surface until now.How
Each endpoint follows the existing
updateSetting/toggleEphemeralpattern: DTO + JSONSchema + router + controller + baileys service call.forwardMessagereuses the privategetMessage(key, full)to load the storedWAMessageand passes it tosendMessage(jid, { forward }).No new dependencies.
tsc --noEmit+tsupbuild pass; pre-commit lint clean.🤖 Generated with Claude Code
Summary by Sourcery
Expand WhatsApp group and messaging APIs while improving read-state reliability, app-state recovery, connection handling, and input protection.
New Features:
Bug Fixes:
Enhancements:
CI:
Tests: